const log4js = require('log4js');
const crypto = require('crypto');
const fs = require('fs');
const nodeUtil = require('util');
const EventEmitter = require('events');

const config = require('./config');

// 日志
log4js.configure(config.log4js_conf);
const accessLogger = log4js.getLogger('access');
const lightLogger = log4js.getLogger('light');

accessLogger.setLevel(config.log_level.access);
lightLogger.setLevel(config.log_level.light);

const errMsg = {
  method_not_null: 'method不能为空',
  request_url_not_null: 'request_url不能为空',
  back_url_not_null: 'back_url不能为空',
  method_not_found: '指定的 method 不存在',
  request_file_fail: '生成请求文件异常',
  request_send_fail: '处理request文件时发送请求异常',
  file_broken: '文件被损坏',
  out_of_sync_count: '强制触发，超过最大同步请求数！',
  request_timeout: '请求超时！',
  unknow_error: '未定义的异常'
};

const logger = lightLogger;
const REQUEST = 'request';
const RESPONSE = 'response';

const unlink = nodeUtil.promisify(fs.unlink);   //删除文件
const readdir = nodeUtil.promisify(fs.readdir);   //读取目录内容
const writeFile = nodeUtil.promisify(fs.writeFile);   //写文件
const stat = nodeUtil.promisify(fs.stat);

// 文件是否存在
const exists = (path) => {
  return new Promise ((suc, fail) => {
    fs.access(path, fs.constants.R_OK | fs.constants.W_OK, (err) => {
      suc(!err)
    })
  })
}

const syncEvent =  new EventEmitter();        //同步请求事件对象，用于发起同步请求时保存通知事件
const SYNC_EVENT_FIX = 'notice-';             //事件名前缀，事件名的格式为 ${notice}-${sequece}

const tmpDir = config.tmp_path;     //临时文件保存目录
if (! fs.existsSync(tmpDir)){
  logger.debug('临时目录不存在，创建 tmp 目录：' + tmpDir);
  fs.mkdirSync(tmpDir);
}

let delQueue = [];    //删除失败的文件缓存队列


//let writeError;          //保存写入时出现的异常，用于判断是否可以写入
let requesteBuf = [];    // 请求文件保存前的缓存
let responseBuf = [];    //响应文件保存前的缓存

let sequence = 1;     //序列
/**
 * 获取一个序列号
 */
function getSequence(){
  return sequence ++;
}

/**
 * 新的请求数据
 */
const pushRequest = (data) => {
  let sequence = getSequence();
  requesteBuf.push({
    sequence, data
  });
  return sequence;
};

/**
 * 新的响应数据
 */
const pushResponse = (sequence, data) => {
  responseBuf.push({
    sequence, data
  });
};

/**
 * 检验文件是否完成
 * @param file 文件路径
 */
const removeFile = async (file)=>{
  logger.info(`删除文件：${file}`);
  try{
    await unlink(file);
  }catch(e){
    logger.error(`文件：${file}，删除失败：`, e);
    //request与response目录也会出现删除失败的情况,把删除失败的文件放入到一个队列中，可以在定时中定时清理
    delQueue.push(file);
  }
}

/**
 * 清理 删除失败的文件缓存队列
 */
const cleanQueue = async ()=>{
  if (!delQueue.length){
    return;
  }
  let queue = [];
  logger.info(`删除失败的文件 ${delQueue.length} 个，重新尝试删除`);
  for(file of delQueue){
    try{
      await unlink(file);
    }catch(e){
      queue.push(file);
    }
  }
  delQueue = queue;
  logger.warn(`无法删除的文件 ${queue.length} 个`);
}

function hexMd5 (str) {
  let md5 = crypto.createHash('md5');
  md5.update(str, 'utf-8');
  return md5.digest('hex');
}

/**
 * 清除目录中的所有文件
 */
const emptyDir = async (path)=>{
  if (await exists(path)){
    let files = await readdir(path);
    let count = files.length;
    let fail = 0;
    let delCount = 0;
    let clearTimestrap = new Date().getTime() - config.clean_cycle;    //此时间之前的文件删除
    logger.info(`删除修改时间 ${new Date(clearTimestrap).format('yyyy-MM-dd HH:mm:ss')} 之前的文件`)
    for(file of files){
      let fix = file.split('.').pop().toLowerCase();
      let filepath = `${path}/${file}`;
      if (fix === REQUEST || fix === RESPONSE){
        //此处可判断文件的创建时间，比如只删除10分钟前创建的文件，这样可避免文件被损坏后找，由于源文件被删除导致数据丢失的情况。
        let fileStat = await stat(filepath);
        if (fileStat.ctimeMs < clearTimestrap){   //使用修改时间比较
            delCount ++;
          //删除文件
          try{
            logger.debug(`删除已传输的文件：${filepath}`);
            await unlink(filepath);
          }catch(e){
            logger.error(`文件：${filepath}，删除失败：`, e);
            fail ++;
          }
        }
      }
    }
    logger.info(`清理目录 ${path} ，一共 ${count} 个文件，需删除 ${delCount}，删除失败 ${fail}`)
  }
}

//生成请求文件
const createRequestFile = async (data)=>{
  let jsonString = JSON.stringify(data);
  let timestamp = new Date().format('yyyyMMddHHmmss');
  let filename = `${timestamp}-${hexMd5(jsonString)}.${REQUEST}`;
  let tmpPath = `${tmpDir}/${filename}`;
  let targetPath = `${config.request_path}/${filename}`;
  logger.info(`生成请求文件：${filename}`);
  //将文件先写入到临时目录，再move到request目录，这样可避免写入文件过大时，文件还未写入完成就被光闸读取到的情况。
  await writeFile(tmpPath, jsonString);       //先把文件写入到临时目录
  //然后把文件从临时目录移动到 request 目录
  fs.rename(tmpPath, targetPath, (e) =>{
    e && logger.error('转移 request 文件异常：', e);
  });
};
//生成响应文件
const createResponseFile = async (data)=>{
  let jsonString = JSON.stringify(data);
  let timestamp = new Date().format('yyyyMMddHHmmss');
  let filename = `${timestamp}-${hexMd5(jsonString)}.${RESPONSE}`;
  let tmpPath = `${tmpDir}/${filename}`;
  //由于响应文件需要经光闸传输到另一端，所以也应该写入到 request 目录
  let targetPath = `${config.request_path}/${filename}`;
  logger.info(`生成请求文件：${filename}`);
  //将文件先写入到临时目录，再move到request目录，这样可避免写入文件过大时，文件还未写入完成就被光闸读取到的情况。
  await writeFile(tmpPath, jsonString);       //先把文件写入到临时目录
  //然后把文件从临时目录移动到 request 目录
  fs.rename(tmpPath, targetPath, (e) =>{
    e && logger.error('转移 request 文件异常：', e);
  });
};

//获取一个异常header
const getErrMsg = (key) =>{
  if (errMsg[key]){
    return {
      code: key,
      message: errMsg[key]
    }
  }else{
    return {
      code: 'unknow_error',
      message: errMsg.unknow_error
    }
  }
}
//定时保存请求与响应文件
setInterval(async () => {
  if (requesteBuf.length){
    logger.debug(`开始保存请求缓存：${requesteBuf.length} 条`)
    try{
      let tmp = requesteBuf;
      requesteBuf = [];
      await createRequestFile(tmp);
    }catch(e){
      logger.error('保存请求缓存失败：', e);
    }
  }
  
  if (responseBuf.length){
    logger.debug(`开始保存响应缓存：${responseBuf.length} 条`)
    try{
      let tmp = responseBuf;
      responseBuf = [];
      await createResponseFile(tmp);
    }catch(e){
      logger.error('保存响应缓存失败：', e);
    }
  }
}, config.save_cycle);

//定时判断同步事件数量，防止内存泄露
setInterval(() => {
  let list = syncEvent.eventNames();
  logger.debug(`现有通知事件：${list ? list.length : 0} 个。`);
  if (list && list.length > config.max_sync_event_count){
    logger.info('通知事件过多，开始清理');
    for (let i=0,len=list.length - config.max_sync_event_count; i<len; i++){
      syncEvent.emit(list[i], {
        header: getErrMsg('out_of_sync_count'),
        sequence: list[i].split('-').pop()
      });
    }
  }
}, config.clear_sync_event_cycle);

module.exports = {
  REQUEST, RESPONSE, logger, removeFile, hexMd5, emptyDir, cleanQueue, pushRequest, pushResponse, createRequestFile, createResponseFile, getErrMsg, exists,
  sucResMsg: {
    code: 'success',
    message: '成功'
  },
  /**
   * 绑定通知事件
   * @param fun 触发事件，包含一个参数 (json) => {}， json为响应结果
   */
  bindNotice: (seq, fun) => {
    syncEvent.once(SYNC_EVENT_FIX + seq, fun);
    logger.debug(`绑定同步请求通知事件：${seq}`);
  },
  //触发通知事件
  emitNotice: (seq, json) => {
    logger.debug(`触发同步请求通知事件：${seq}`)
    let b = syncEvent.emit(SYNC_EVENT_FIX + seq, json);
    b || logger.warn(`同事通知事件 seq=${seq} 触发失败，json=`, json);
  }
}


Date.prototype.format = function(formatStr)
{
    var str = formatStr;
    var Week = ['日', '一', '二', '三', '四', '五', '六'];

    str = str.replace(/yyyy|YYYY/, this.getFullYear());
    str = str.replace(/yy|YY/, (this.getYear() % 100) > 9 ? (this.getYear() % 100).toString() : '0' + (this.getYear() % 100));

    var month = this.getMonth() + 1;
    str = str.replace(/MM/, month > 9 ? month.toString() : '0' + month);
    str = str.replace(/M/g, month);

    str = str.replace(/w|W/g, Week[this.getDay()]);

    str = str.replace(/dd|DD/, this.getDate() > 9 ? this.getDate().toString() : '0' + this.getDate());
    str = str.replace(/d|D/g, this.getDate());

    str = str.replace(/hh|HH/, this.getHours() > 9 ? this.getHours().toString() : '0' + this.getHours());
    str = str.replace(/h|H/g, this.getHours());
    str = str.replace(/mm/, this.getMinutes() > 9 ? this.getMinutes().toString() : '0' + this.getMinutes());
    str = str.replace(/m/g, this.getMinutes());

    str = str.replace(/ss|SS/, this.getSeconds() > 9 ? this.getSeconds().toString() : '0' + this.getSeconds());
    str = str.replace(/s|S/g, this.getSeconds());

    return str;
};
